package paim.wingchun.controller;


import java.beans.PropertyVetoException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Stack;
import java.util.logging.Level;

import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.event.EventListenerList;
import javax.swing.event.InternalFrameEvent;
import javax.swing.event.InternalFrameListener;

import org.hibernate.Session;

import com.google.common.collect.Collections2;

import paim.wingchun.app.Hibernates;
import paim.wingchun.app.Reflections;
import paim.wingchun.app.WC_App;
import paim.wingchun.app.WC_Log;
import paim.wingchun.controller.eventos.DefaultInternalFrameEvent;
import paim.wingchun.controller.eventos.DefaultModalInternalFrameEvent;
import paim.wingchun.model.Funcoes;
import paim.wingchun.model.Recognizable;
import paim.wingchun.model.modelos.Model;
import paim.wingchun.model.pojos.Erro;
import paim.wingchun.model.pojos.Pojo;
import paim.wingchun.view.AbstractView;
import paim.wingchun.view.Dialogs;
import paim.wingchun.view.Fireable;

/** @author paim 18/01/2011
 * @param <P>
 *            POJO
 * @param <V>
 *            Visao */

/* FIXME, poderia deixar tudo aqui em termos de object */

public abstract class AbstractController<P extends Pojo, V extends AbstractView<P>> implements Recognizable<P>,
                InternalFrameListener, Fireable<ControllerListener> {

    public enum Inclusao {
        NAO_INCLUI, CONFIRMA, NAO_CONFIRMA
    }

    public enum Exclusao {
        NAO_EXCLUI, CONFIRMA, NAO_CONFIRMA
    }

    public enum Eventos {
        /* CONFIRMAR, CANCELAR, */NAVEGAR, INCLUIR, EXCLUIR, /** necessario para controlador saber que houve alteracao */
        ALTERANDO, EDITANDO
    }

    private final Class<P> pClass;

    private final Class<V> vClass;

    private final Class<? extends Model<P>> mClass;

    protected V visao;

    private Model<P> modelo;

    private Session sessao;

    /* Create the listener list */
    private EventListenerList listenerList = new EventListenerList();

    private AbstractController<? extends Pojo, ? extends AbstractView<? extends Pojo>> pai;

    private List<AbstractController<?, ?>> filhos = new ArrayList<>();

    private JComponent componentPai;

    private boolean cancelarFechaFilho = true;

    private boolean confirmarFechaFilho = true;

    /** pilha de bloqueio de entrada de eventos de alteracao de pojo */
    private Stack<Boolean> dispara = new Stack<Boolean>();

    private Pojo pojoPai;

    public Pojo getPojoPai() {
        return this.pojoPai;
    }

    // FIXME acho que pode estacionar esse mclass, pois cada pojo esta associado a um model. ok

    protected AbstractController() {
        this.pClass = Reflections.getClassParameter(this.getClass(), Pojo.class);
        this.vClass = Reflections.getClassParameter(this.getClass(), AbstractView.class);
        this.mClass = Reflections.getModelClass(pClass);
        inicializa();
    }

    @Deprecated
    protected AbstractController(Class<P> pClass, Class<V> vClass) {
        this(pClass, vClass, Reflections.getModelClass(pClass));
    }

    protected <M extends Model<P>> AbstractController(Class<P> pClass, Class<V> vClass, Class<M> mClass) {
        this.pClass = pClass;
        this.vClass = vClass;
        this.mClass = mClass;
        inicializa();
    }

    protected <V2 extends AbstractView<P>> AbstractController(AbstractController<P, V2> parent) {
        this.pClass = parent.getpClass();
        this.vClass = Reflections.getClassParameter(this.getClass(), AbstractView.class);
        this.mClass = parent.getmClass();
        this.pai = parent;
        inicializa();
    }

    @Deprecated
    /** Controlador com pai, que nao tem a mesma visao que o filho, mas que usa mesmo modelo e pojo nao necessariamente o */
    protected <C extends AbstractController<P, ?> & HasConstrutor> AbstractController(C parent, Class<V> vClass) {
        this(parent, parent.getpClass(), vClass, parent.getmClass());
    }

    @Deprecated
    protected <C extends AbstractController<?, ?> & HasConstrutor> AbstractController(C parent, Class<P> pClass,
                    Class<V> vClass, Class<? extends Model<P>> mClass) {
        this(parent, null, null, null, pClass, vClass, mClass);
    }

    protected <P2 extends Pojo, V2 extends AbstractView<P2>, C extends AbstractController<P2, V2> & HasConstrutor> AbstractController(
                    C parent, P2 parentPojo, JComponent parentComponent, Class<P> pClass) {
        this.pClass = pClass;
        this.vClass = Reflections.getClassParameter(this.getClass(), AbstractView.class);
        this.mClass = Reflections.getModelClass(pClass);
        this.pai = parent;
        this.componentPai = parentComponent;
        this.pojoPai = parentPojo;
        inicializa();
    }

    @Deprecated
    /** Controlador com pai que implementa HasConstrutor e que tem POJO, Visao e Modelo diferentes do filho */
    protected <C extends AbstractController<?, ?> & HasConstrutor> AbstractController(C parent, Pojo pojoPai,
                    JComponent componentePai, Class<P> pClass, Class<V> vClass, Class<? extends Model<P>> mClass) {
        this(parent, null, pojoPai, componentePai, pClass, vClass, mClass);
    }

    @Deprecated
    /** Controlador com pai que implementa HasConstrutor e que tem POJO, Visao e Modelo diferentes do filho */
    protected <C extends AbstractController<?, ?> & HasConstrutor> AbstractController(C parent, Session sessaoPai,
                    Pojo pojoPai, JComponent parentComponent, Class<P> pClass, Class<V> vClass,
                    Class<? extends Model<P>> mClass) {
        this.pClass = pClass;
        this.vClass = vClass;
        this.mClass = mClass;
        this.pai = parent;
        this.componentPai = parentComponent;
        this.pojoPai = pojoPai;
        inicializa();
    }

    protected final void inicializa() {

        /* impede a criacao de um controlador se ele ja existe e ainda jogar o existente para frente */
        if ( WC_App.getControladors().contains(this) ) {
            for ( Iterator<?> iterator = WC_App.getControladors().iterator(); iterator.hasNext(); ) {
                AbstractController<?, ?> cExistente = (AbstractController<?, ?>) iterator.next();
                if ( this.equals(cExistente) ) {
                    WC_App.getInstancia().getVisaoApp().getjDesktopPane().moveToFront(cExistente.getVisao());
                    try {
                        /* suicidio */
                        this.finalize();
                        return;
                    }
                    catch ( Throwable e ) {
                        WC_Log.getInstancia().getlogger().log(Level.SEVERE, e.getMessage(), e);
                        return;
                    }
                }
            }
        }

        try {
            this.sessao = instanciaSessao();
            this.modelo = instanciaModelo();
            this.visao = instanciaVisao();
            configuraListeners();
            populaVisao();
            ajustaVisao();
        }
        catch ( Exception e ) {
            WC_Log.getInstancia().getlogger().log(Level.SEVERE, e.getMessage(), e);
        }
    }

    /** ajustes nessa visao e como adicionala a aplicacao, inclusao dos ultimos listeners
     *
     * @author paim 30/05/2011
     * @throws PropertyVetoException
     *             void */
    private void ajustaVisao() throws PropertyVetoException {
        final int xOffset = 30, yOffset = 30;
        int openFrameCount = WC_App.getControladors().size();

        getVisao().setLocation(xOffset * openFrameCount, yOffset * openFrameCount);

        /* enable glassPane -->> modal */
        if ( getPai() != null && isModal() ) {
            /* cria um painel glass */
            final JPanel glassPanel = getVisao().getGlassPanel();

            /* adiciona eventos especificos de janela modal */
            getVisao().addInternalFrameListener(new DefaultModalInternalFrameEvent(this, glassPanel));

            /* adiciona a visao no painel glass */
            glassPanel.add(getVisao());
            getPai().getVisao().setGlassPane(glassPanel);

            glassPanel.setVisible(true);
            getVisao().setSelected(true);
            // ######################################
            WC_App.getInstancia().getVisaoApp().getjDesktopPane().add(getVisao(), JLayeredPane.MODAL_LAYER);
        }
        else {
            getVisao().setSelected(true);
            WC_App.getInstancia().getVisaoApp().getjDesktopPane().add(getVisao(), JLayeredPane.PALETTE_LAYER);
        }

        /* adiciona listener padrao de qualquer janela InternalFrame. Obs.: deve ser o ultimo listener desse tipo a ser
         * adicionado */
        getVisao().addInternalFrameListener(new DefaultInternalFrameEvent(this));

        /* mostra a visao */
        getVisao().toFront();
        getVisao().setVisible(true);

        /* coloca janela na frente */
        WC_App.getInstancia().getVisaoApp().getjDesktopPane().moveToFront(getVisao());
        getVisao().setMaximumSize(getVisao().getContentPane().getSize());
        getVisao().setMaximizable(true);
        getVisao().setOpaque(true);

        /* adiciona a lista de controladores ativos */
        WC_App.getControladors().add(this);
    }

    public boolean isModal() {
        return false;
    }

    public V getVisao() {
        return visao;
    }

    public void setVisao(V visao) {
        this.visao = visao;
    }

    public void setModelo(Model<P> modelo) {
        this.modelo = modelo;
    }

    public Model<P> getModelo() {
        return modelo;
    }

    @Override
    public final Class<P> getpClass() {
        return pClass;
    }

    public final Class<? extends Model<P>> getmClass() {
        return mClass;
    }

    public final Class<V> getvClass() {
        return vClass;
    }

    protected void setSessao(Session sessao) {
        this.sessao = sessao;
    }

    public Session getSessao() {
        return sessao;
    }

    public AbstractController<?, ?> getPai() {
        return pai;
    }

    public JComponent getComponentPai() {
        return componentPai;
    }

    public Session instanciaSessao() {
        if ( getPai() != null ) {
            return getPai().getSessao();
        }
        else {
            return Hibernates.openSession();
        }
    }

    public final V instanciaVisao() throws Exception {
        antes_instanciaVisao();
        V visao = c_instanciaVisao();
        visao = apos_instanciaVisao(visao);

        return visao;
    }

    protected void antes_instanciaVisao() {}

    protected V c_instanciaVisao() throws Exception {
        try {
            Constructor<V> construtor = getvClass().getDeclaredConstructor();
            V visao = construtor.newInstance();
            return visao;
        }
        catch ( NoSuchMethodException e ) {
            Constructor<V> construtor = getvClass().getDeclaredConstructor(new Class[] { Class.class });
            V visao = construtor.newInstance(getpClass());
            return visao;
        }
    }

    protected V apos_instanciaVisao(V v) {
        return v;
    }

    // TODO por enquanto prevendo apenas CSimple e CList CUniq, pois tratam apenas de uma classe de objeto
    /** instancia o modelo principal */
    public final <M extends Model<P>> M instanciaModelo() throws Exception {
        antes_instanciaModelo();
        M modelo = c_instanciaModelo();
        modelo = apos_instanciaModelo(modelo);
        return modelo;
    }

    protected <M extends Model<P>> M c_instanciaModelo() throws Exception {
        Method getInstancia = getmClass().getMethod("getInstance", (Class[]) null);
        @SuppressWarnings("unchecked")
        M instanciaModelo = (M) getInstancia.invoke(getmClass(), (Object[]) null);
        return instanciaModelo;
    }

    protected void antes_instanciaModelo() {}

    protected <M extends Model<P>> M apos_instanciaModelo(M modelo) {
        return modelo;
    }

    /** seta objeto na visao */
    public final void setObjeto(Object objeto) {
        objeto = antes_setObjeto(objeto);
        getVisao().setObjeto(objeto);
        apos_setObjeto(objeto);
    }

    protected Object antes_setObjeto(Object objeto) {
        return objeto;
    }

    protected void apos_setObjeto(Object objeto) {}

    /** busca objetos visualizados */
    public final Object getObjeto() {
        antes_getObjeto();
        Object objeto = getVisao().getObjeto();
        objeto = apos_getObjeto(objeto);

        return objeto;
    }

    protected void antes_getObjeto() {}

    protected Object apos_getObjeto(Object objeto) {
        return objeto;
    }

    public final AbstractView.Estado confirmar() {
        AbstractView.Estado confirmado = AbstractView.Estado.Alterado;
        freeze();
        try {
            Object object = getObjeto();

            List<Erro> erros = refreshErros(object);

            if ( antes_confirmar(object, erros) ) {
                confirmado = c_confirmar(object, erros);
                apos_confirmar(object, erros, confirmado);
                if ( AbstractView.Estado.Confirmado.equals(confirmado) && getPai() instanceof HasConstrutor ) {
                    devolver(object);
                }
            }

        }
        catch ( Exception e ) {
            WC_Log.getInstancia().getlogger().log(Level.SEVERE, e.getMessage(), e);
        }
        finally {
            warmup();
        }
        return confirmado;
    }

    /** se falso, nao executa {@link #c_confirmar(Object, List)} ou {@link #apos_confirmar(Object, List, Estado)} */
    protected boolean antes_confirmar(Object objeto, List<Erro> erros) {
        return true;
    }

    protected void apos_confirmar(Object objeto, List<Erro> erros, AbstractView.Estado estado) {}

    protected abstract AbstractView.Estado c_confirmar(final Object objeto, List<Erro> erros) throws Exception;

    /** @author paim 25/10/2011
     * @param objeto
     * @throws Exception
     *             void */
    protected abstract void devolver(Object objeto) throws Exception;

    public final boolean cancelar() {
        Boolean cancelado = null;
        freeze();

        try {
            Object objeto = getObjeto();
            if ( antes_cancelar(objeto) ) {
                cancelado = c_cancelar(objeto);
                apos_cancelar(cancelado, objeto);
                return cancelado;
            }
            else
                return false;
        }
        catch ( Exception e ) {
            // TODO: handle exception
            if ( cancelado == null )
                return false;
            return cancelado;
        }
        finally {
            warmup();
        }
    }

    /** se falso nao executa {@link #c_cancelar(Object)} ou o {@link #apos_cancelar(boolean, Object)} */
    protected boolean antes_cancelar(Object objeto) {
        return true;
    }

    protected void apos_cancelar(boolean cancelado, Object objeto) {}

    protected abstract boolean c_cancelar(Object objeto) throws Exception;

    /** inclui um objeto da classe {@link #getpClass()}
     *
     * @author paim 03/05/2011
     * @return boolean */
    public final boolean incluir() {
        Boolean incluido = null;
        freeze();

        try {
            P novoPojo = getpClass().newInstance();
            Inclusao inclusao = antes_incluir(novoPojo);

            switch ( inclusao ) {
                case NAO_INCLUI:
                    apos_incluir(false, novoPojo);
                    return false;
                case CONFIRMA:
                    if ( !getVisao().confirmaInclusao() )
                        return false;
                case NAO_CONFIRMA:
                default:
                    incluido = c_incluir(novoPojo);
                    apos_incluir(incluido, novoPojo);
                    return incluido;
            }
        }
        catch ( Exception e ) {
            if ( incluido == null )
                return false;
            return incluido;
        }
        finally {
            warmup();
        }
    }

    protected Inclusao antes_incluir(P novoPojo) throws Exception {
        return Inclusao.NAO_CONFIRMA;
    }

    protected void apos_incluir(boolean incluido, P novoPojo) {}

    protected abstract boolean c_incluir(P novoPojo) throws Exception;

    public final boolean excluir() {

        Boolean excluido = null;
        freeze();
        try {
            Object objeto = getObjeto();
            List<Erro> erros = new ArrayList<Erro>();

            if ( validaExclusao() )
                erros = refreshErros(objeto);

            Exclusao exclusao = antes_excluir(objeto, erros);

            switch ( exclusao ) {
                case NAO_EXCLUI:
                    return false;

                case CONFIRMA:
                    if ( !getVisao().confirmaExclusao() )
                        return false;

                case NAO_CONFIRMA:

                default:
                    excluido = c_excluir(objeto, erros);
                    if ( !excluido ) {
                        List<Erro> errosImpeditivos = (List<Erro>) Collections2.filter(erros,
                                        Funcoes.ERRO.impeditivos());
                        if ( !errosImpeditivos.isEmpty() )
                            Dialogs.showMessageErros(this.getVisao(), errosImpeditivos);
                        else
                            Dialogs.erro("por que?", "Exclusão mal sucedida");
                    }
                    apos_excluir(excluido);
                    return excluido;
            }
        }
        catch ( Exception e ) {
            if ( excluido == null )
                return false;
            return excluido;
        }
        finally {
            warmup();
        }
    }

    /** true se exclusao bem sucedida
     *
     * @author paim 28/04/2011
     * @param objeto
     * @param errosExclusao
     * @return boolean
     * @throws Exception */
    protected boolean c_excluir(Object objeto, List<Erro> errosExclusao) throws Exception {
        /* se for um pojo ou lista */
        getModelo().delete(getSessao(), objeto, errosExclusao);
        return true;
    }

    /** @author paim 29/04/2011
     * @param objeto
     *            que sera removido
     * @param erros
     *            pode se adicionar algun erro em tempo de controlador
     * @return Exclusao */
    protected Exclusao antes_excluir(Object objeto, List<Erro> erros) {
        return Exclusao.CONFIRMA;
    }

    protected void apos_excluir(boolean excluido) {}

    /** deve validar antes de excluir ou pode remover de qualquer jeito
     *
     * @author paim 29/04/2011
     * @return boolean */
    protected boolean validaExclusao() {
        return true;
    }

    @Deprecated
    protected void antesValidar(Object objeto) {}

    /** valida o objeto */
    public final List<Erro> validar(Object objeto) {
        List<Erro> erros = new ArrayList<Erro>();
        antesValidar(objeto);
        /* se for um pojo */
        if ( Pojo.class.isAssignableFrom(objeto.getClass()) ) {
            List<Erro> e = getModelo().validar(sessao, (Pojo) objeto);
            erros.addAll(e);
        }
        /* se for uma lista */
        else if ( List.class.isAssignableFrom(objeto.getClass()) ) {
            List<?> pojos = (List<?>) objeto;
            for ( Object pojo : pojos ) {
                List<Erro> es = getModelo().validar(sessao, (Pojo) pojo);
                erros.addAll(es);
            }
        }
        aposValidar(objeto, erros);
        return erros;
    }

    @Deprecated
    protected void aposValidar(Object objeto, List<Erro> erros) {}

    public final void configuraListeners() throws Exception {
        if ( antes_addListeners() ) {
            getVisao().addInternalFrameListener(this);
            /* adiciona os listener que app pode inferir */
            addListeners();
        }
        apos_addListeners();
    }

    abstract void addListeners() throws Exception;

    protected boolean antes_addListeners() {
        return true;
    }

    protected void apos_addListeners() {}

    public final void populaVisao() throws Exception {
        freeze();
        try {
            if ( antes_populaVisao() ) {
                Object o = cPopulaVisao();
                getVisao().setEditando(false);
                apos_populaVisao(o);
            }
            else {
                P o = getpClass().newInstance();
                setObjeto(o);
                getVisao().setEditando(true);
                apos_populaVisao(o);
            }
            getVisao().vCleanErros();
        }
        finally {
            warmup();
        }
    }

    /** se true entao visao eh populada com valores do banco se false popula com novo objeto
     *
     * @throws Exception */
    protected boolean antes_populaVisao() throws Exception {
        return true;
    }

    abstract Object cPopulaVisao() throws Exception;

    protected void apos_populaVisao(Object objeto) throws Exception {}

    public boolean isCancelarFechaFilho() {
        return this.cancelarFechaFilho;
    }

    public void setCancelarFechaFilho(boolean fechar) {
        this.cancelarFechaFilho = fechar;
    }

    public boolean isConfirmarFechaFilho() {
        return this.confirmarFechaFilho;
    }

    public void setConfirmarFechaFilho(boolean fechar) {
        this.confirmarFechaFilho = fechar;
    }

    public List<Erro> refreshErros(Object objeto) {
        List<Erro> erros = validar(objeto);
        getVisao().refreshErros(erros);
        return erros;
    }

    public void refreshErrosSlow(final Object objeto) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                refreshErros(objeto);
            }
        });
    }

    /** retorna o topo da pilha, ou seja o ultimo estado <BR>
     * se true Permite o disparo de eventos de mudanca de conteudo
     *
     * @author temujin Jun 28, 2011
     * @return boolean */
    public boolean canDisparaEvento() {
        try {
            /* retorna o topo da pilha, ou seja o ultimo estado */
            return dispara.peek();
        }
        catch ( EmptyStackException e ) {
            /* se pilha estiver vazia, retornar true, pois nao ha empecilhos */
            return true;
        }
    }

    /** impede a entrada de acoes na interface grafica para this
     *
     * @author temujin Jun 28, 2011 void */
    public void freeze() {
        /* empurra um false no topo da pilha */
        dispara.push(Boolean.FALSE);
        this.getVisao().freeze();
    }

    /** remove o ultimo da pilha, retornando ao estado anterior
     *
     * @author temujin Jun 28, 2011 void */
    public void warmup() {
        /* remove o ultimo da pilha, retornando ao estado anterior */
        dispara.pop();
        this.getVisao().warmup();
    }

    @Override
    public EventListenerList getListenersList() {
        return listenerList;
    }

    @Override
    public void addListener(ControllerListener listener) {
        listenerList.add(ControllerListener.class, listener);
    }

    @Override
    public void removeListener(ControllerListener listener) {
        listenerList.remove(ControllerListener.class, listener);
    }

    @Override
    public void fire(WC_Event evt) {
        ControllerListener[] listeners = getListenersList().getListeners(ControllerListener.class);
        for ( ControllerListener listener : listeners ) {
            listener.eventOccurred(evt);
        }
    }

    @Override
    public void internalFrameOpened(InternalFrameEvent e) {}

    @Override
    public void internalFrameClosing(InternalFrameEvent e) {}

    @Override
    public void internalFrameClosed(InternalFrameEvent e) {}

    @Override
    public void internalFrameIconified(InternalFrameEvent e) {}

    @Override
    public void internalFrameDeiconified(InternalFrameEvent e) {}

    @Override
    public void internalFrameActivated(InternalFrameEvent e) {}

    @Override
    public void internalFrameDeactivated(InternalFrameEvent e) {}

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.pai == null) ? 0 : this.pai.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if ( this == obj )
            return true;

        if ( obj == null )
            return false;

        if ( getClass() != obj.getClass() )
            return false;

        if ( !Objects.equals(this, obj) )
            return false;

        return true;
    }

}
